Skip to content

feat: per-workspace routing via X-Mcpmux-Workspace header + guided folder setup - #182

Merged
its-mash merged 14 commits into
mainfrom
feat/workspace-header-mapping
Jun 24, 2026
Merged

feat: per-workspace routing via X-Mcpmux-Workspace header + guided folder setup#182
its-mash merged 14 commits into
mainfrom
feat/workspace-header-mapping

Conversation

@its-mash

Copy link
Copy Markdown
Member

Why

Clients that don't report MCP roots reliably — notably Cursor, which multiplexes one MCP host across windows with roots.listChanged=false — could not be routed to the right workspace binding, so @mux returned another window's FeatureSet. roots is also deprecated (SEP-2577) and sessions are being removed (SEP-2575/2567), so connection-carried identity, not the client's reported roots, is the durable signal.

What

Gateway

  • X-Mcpmux-Workspace header routing. The header value is the workspace folder path; the OAuth middleware pins it into SessionRootsRegistry, where it shadows the client's probed roots in get(). The resolver, probe-skip, and prompt-root all read through get(), so the header flows through Tier 1 unchanged — authoritative over a stale/absent reported root. Coexists with roots-based mapping (header wins); no new tier, param, or DB migration.
  • Optional system-wide disable of inbound auth (gateway.auth_disabled, default off). Middleware is now lenient: a valid token is always honored, but with auth off a tokenless client is accepted as an anonymous identity on the default Space. Seeded at startup, flipped live.

Desktop

  • Per-workspace installer: writes/extends project-local MCP configs (Cursor, Claude Code, VS Code/Copilot, opencode, Zed) with the workspace header — merge-not-clobber, backups, refuses non-JSON. Copy-snippet per client; remembers the last client selection.
  • Guided "Set up a folder" walkthrough (create path; editing keeps the inspector): Folder → Connect apps → Tools (defaults to Starter, one-click Finish). After Finish it lands on the new mapping's inspector showing its effective features. A home "Set up a folder" CTA opens it.
  • Settings → Security toggle for disabling auth; the install panel routes there (deep-links + flashes the section) rather than flipping it inline.

Testing

  • Rust: resolver header-priority + coexistence cases; registry pin/shadow/clear; GatewayState toggle; config-writer merge/create/extend/backup/reject-JSONC; authless HTTP integration test driving the real middleware (tokenless /mcp → 200 + anonymous when disabled, 401 when required).
  • TS: install panel (install/copy/auth-route-to-settings/remembered selection/gateway-gating) and the setup walkthrough (3 steps, Finish creates binding, Back).
  • Full suites green locally: 631 Rust, 220 TS.
  • Manual guide: docs/manual/workspace-header-routing.md.

Notes

  • roots-reporting clients (VS Code/Claude Code) are unaffected — the pin only applies when the header is present.
  • Claude Desktop (stdio, no static headers) and global-only clients (Windsurf/Cline) are intentionally excluded from the per-workspace installer.

https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF

its-mash added 14 commits June 24, 2026 12:51
Clients that don't report MCP `roots` reliably — notably Cursor, which
multiplexes one MCP host across windows with `roots.listChanged=false` —
could not be routed to the right workspace binding, so `@mux` saw another
window's FeatureSet. `roots` is also deprecated (SEP-2577) and sessions are
being removed (SEP-2575/2567), so connection-carried identity, not the
client's roots, is the durable signal.

Add an `X-Mcpmux-Workspace` request header whose value is the workspace root
path. The OAuth middleware pins it into SessionRootsRegistry, where it
shadows the client's probed roots in `get()`. Because the resolver, the
on-demand probe skip, and the prompt-root derivation all read roots through
`get()`, the header flows through Tier 1 unchanged — exact binding match,
else Space default — and is authoritative over a stale or absent reported
root. No new resolver tier, parameter, or DB migration; the header value is
just a workspace root normalized like any other.

Tests: registry pin/shadow/clear units; resolver integration cases proving a
pinned root routes to its binding with no reported roots, overrides a
conflicting reported root, and falls back to Space default when unmapped.

Claude-Session: https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
Add a `gateway.auth_disabled` setting (default false — auth required) that,
when on, lets inbound MCP clients connect without a Bearer token. This makes
the upcoming one-click per-workspace install trivial: a client config needs
only the URL + `X-Mcpmux-Workspace` header, no OAuth/access-key dance.

The middleware is now lenient rather than all-or-nothing: a valid token is
always honored when present, so flipping the setting never breaks an
already-configured client. With auth disabled and no valid token, the
connection is accepted as an anonymous client on the default Space; routing
still prefers the workspace header → binding. The toggle lives in
GatewayState (seeded from settings at startup, flipped live by
set_gateway_auth_disabled so no restart is needed).

Claude-Session: https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
Add commands to register the gateway endpoint in a project-local MCP config
inside a chosen folder, injecting `X-Mcpmux-Workspace: <folder path>` so the
gateway routes the connection to that folder's workspace binding without
relying on the client reporting MCP roots.

- list_workspace_install_clients: the supported project-local clients
  (Cursor, Claude Code, VS Code/Copilot, opencode, Zed) with their config
  paths. Each client's config-shape differences (top-level key mcpServers /
  servers / mcp / context_servers, and the type field) live in one table.
- generate_workspace_config_snippet: copy-paste full-file snippet per client.
- install_workspace_mcp_config: create or extend each client's config,
  preserving other servers, backing up an existing file, creating parent
  dirs. Refuses to clobber a non-JSON (JSONC) file, reporting it instead.

Global-only clients (Windsurf, Cline) and Claude Desktop (stdio, no static
headers) are excluded — a per-workspace header needs project-local scope.

Tested: entry shape per client, merge create/extend/replace, preserve
siblings, reject JSONC/non-object, create-then-update-with-backup on disk.

Claude-Session: https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
Frontend for the per-workspace MCP install:

- WorkspaceInstallPanel: a "Connect apps to this folder" section in the
  Workspaces inspector. Lists the supported clients (Cursor, Claude Code,
  VS Code/Copilot, opencode, Zed), copies a per-client snippet, and writes
  the configs into the folder with this folder's path as the
  X-Mcpmux-Workspace header. Self-introductory: shows the system-wide auth
  state and offers an inline "Disable authentication" action, since turning
  it off makes the written config a pure URL + header.
- Settings → Security: a "Disable authentication" toggle.
- workspaceInstall API wrappers for the new commands.

Tested (vitest): lists clients, installs the default-selected clients with
the gateway /mcp url, flips auth inline, copies a snippet, and blocks install
until the gateway is running.

Claude-Session: https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
- Approval sheet and the Apps page's "Routing is workspace-driven" panel now
  point users at "Connect apps to this folder" when a client (e.g. Cursor)
  doesn't report the folder reliably.
- Add a manual test guide for header routing, the per-workspace installer,
  and the disable-auth toggle, with log lines to look for.

Claude-Session: https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
The "Connect apps to this folder" checklist defaulted to the common three
every time. Persist the user's selection to localStorage (matching how the
app stores other UI prefs) and restore it across folders and sessions,
reconciled against the currently-supported clients so a dropped client can't
leave a stale id (falling back to available defaults if pruning empties it).

Test: deselect to one client + install, remount, and confirm the remembered
selection is restored rather than the default.

Claude-Session: https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
Lock in the requirement that folder-path/roots-reporter mapping keeps working
alongside the X-Mcpmux-Workspace header, with the header taking priority. One
session: with no pin it resolves via its reported root; pinning a header root
for a different folder then overrides it. Guards against the pin ever becoming
unconditional and breaking roots-reporting clients.

Claude-Session: https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
Authentication is an application-wide control, so the per-workspace install
panel no longer flips it inline. The auth nudge now links to Settings →
Security (via navigateTo('settings')) where the user makes the deliberate
choice. Drops the inline setGatewayAuthDisabled call from the panel.

Test updated: the nudge renders an "Open Settings" action that navigates to
settings rather than toggling auth in place.

Claude-Session: https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
Reframe the auth notice from "Apps will need an access key" to the actual
one-time flow: you enable and authenticate the app once and then it connects,
and the client authentication requirement can be disabled in Settings →
Security for a no-key setup.

Claude-Session: https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
Replace the multi-paragraph auth notice with a single concise line:
"Enable and authenticate this app once to connect — or disable the
requirement in Settings."

Claude-Session: https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
Reword the intro and remove the "(like Cursor)" example — keep it generic:
"…so it routes here automatically — even apps that don't report the folder."

Claude-Session: https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
"Open Settings" from the install panel now lands on the Security section
instead of the top of Settings. A `pendingSettingsSection` store signal is
set to 'security' before navigating; SettingsPage scrolls that section into
view and briefly flashes a ring around it, then clears the signal.

Also tighten the Disable-authentication help text to two lines.

Claude-Session: https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
New mapping (create path) is now a 3-step walkthrough; editing an existing
mapping still uses the inspector.

  1. Folder  — required; pick via dialog or a detected workspace.
  2. Apps    — optional; the install panel writes the per-workspace config
               (X-Mcpmux-Workspace header) so apps route here.
  3. Tools   — defaults to the Space's Starter so Finish is one click; the
               binding is created on Finish.

Abandoning before Finish is safe (the folder uses the default Starter until
mapped, and any installed config still points at it).

A "Set up a folder" card on Home opens the walkthrough via a
pendingWorkspaceNew store signal (WorkspacesPage launches it on arrival).

Tests: wizard walks the 3 steps and Finish creates the binding with the
folder path + chosen Space + default Starter; Back navigation. Updated the
three store-mocking suites for the new selectors.

Claude-Session: https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
…s gateway

- After the setup walkthrough's Finish, land on the new mapping's inspector
  (which shows its effective features) instead of just closing. The wizard no
  longer self-closes on create; the page transitions to the created entry.
- Prove the gateway is truly authless when inbound auth is disabled: a new
  HTTP integration test drives the REAL oauth middleware and asserts a
  tokenless POST to /mcp is accepted (200, anonymous identity injected) when
  disabled, and rejected (401) when auth is required.

Claude-Session: https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
@its-mash
its-mash merged commit e2ec055 into main Jun 24, 2026
13 checks passed
@its-mash
its-mash deleted the feat/workspace-header-mapping branch June 24, 2026 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant